package JasPerl::PageContext;

use strict;
use warnings;

use parent qw(Exporter);

use JasPerl::Exceptions;
use JasPerl::ErrorData;
use JasPerl::JspWriter;
use JasPerl::RequestDispatcher;
use JasPerl::Util::Attributes;
use JasPerl::PageContext::PageWriter;
use JasPerl::Response;

use JasPerl::PageContext::HeaderHash;
use JasPerl::PageContext::ParamHash;
use JasPerl::PageContext::ScopeHash;

use constant {
    PAGE_SCOPE        => 0,
    REQUEST_SCOPE     => 1,
    SESSION_SCOPE     => 2,
    APPLICATION_SCOPE => 3
};

use constant {
    APPLICATION => "javax.servlet.jsp.jspApplication",
    CONFIG      => "javax.servlet.jsp.jspConfig",
    EXCEPTION   => "javax.servlet.jsp.jspException",
    OUT         => "javax.servlet.jsp.jspOut",
    PAGE        => "javax.servlet.jsp.jspPage",
    PAGECONTEXT => "javax.servlet.jsp.jspPageContext",
    REQUEST     => "javax.servlet.jsp.jspRequest",
    RESPONSE    => "javax.servlet.jsp.jspResponse",
    SESSION     => "javax.servlet.jsp.jspSession"
};

our $VERSION = 0.1;

our %EXPORT_TAGS = (
    scopes     => [ qw(PAGE_SCOPE REQUEST_SCOPE SESSION_SCOPE APPLICATION_SCOPE) ],
    attributes => [ qw(APPLICATION CONFIG EXCEPTION OUT PAGE PAGECONTEXT REQUEST RESPONSE SESSION) ]
);

our @EXPORT_OK = map { @{$_} } values %EXPORT_TAGS;

my %NAMED_SCOPES = (
    page        => PAGE_SCOPE,
    request     => REQUEST_SCOPE,
    session     => SESSION_SCOPE,
    application => APPLICATION_SCOPE
);

my @INVALID_SCOPE_MESSAGES = (
    'page scope not available',
    'request scope not available',
    'session scope not available',
    'application scope not available',
);

my %IMPLICIT_OBJECTS = (
    pageContext => sub {
        return $_[0];
    },
    pageScope => sub {
        return JasPerl::PageContext::ScopeHash->new_hash($_[0], PAGE_SCOPE);
    },
    requestScope => sub {
        return JasPerl::PageContext::ScopeHash->new_hash($_[0], REQUEST_SCOPE);
    },
    sessionScope => sub {
        # FIXME: ??? check spec ???
        return $_[0]->get_session() ?
            JasPerl::PageContext::ScopeHash->new_hash($_[0], SESSION_SCOPE) :
            undef;
    },
    applicationScope => sub {
        return JasPerl::PageContext::ScopeHash->new_hash($_[0], APPLICATION_SCOPE);
    },
    param => sub {
        return JasPerl::PageContext::ParamHash->new_hash($_[0]);
    },
    paramValues => sub {
        return JasPerl::PageContext::ParamHash->new_hash($_[0], 1);
    },
    header => sub {
        return JasPerl::PageContext::HeaderHash->new_hash($_[0]);
    },
    headerValues => sub {
        return JasPerl::PageContext::HeaderHash->new_hash($_[0], 1);
    },
    cookie => sub {
        my $request = $_[0]->get_request()
            or return;
        my $cookies = $request->get_cookies()
            or return;
        return { map { $_->get_name() => $_ } @{$cookies} };
    },
    initParam => sub {
        my $page = $_[0]->get_page()
            or return;
        my $names = $page->get_init_parameter_names()
            or return;
        return { map { $_ => $page->get_init_parameter($_) } $names->list };
    }
);

sub new {
    my $class = shift;

    my $self = bless { }, $class;
    $self->init(@_) if @_;
    return $self;
}

sub init {
    my ($self, $page, $request, $response, $errpage, $need_session, @bufargs) = @_;

    my $config = $page->get_config()
        or JasPerl::Util::IllegalStateException->throw('no page config');
    my $context = $config->get_context()
        or JasPerl::Util::IllegalStateException->throw('no application context');
    my $out = JasPerl::PageContext::PageWriter->new($response, @bufargs);

    my $session;

    # try to get session unless explicitly disabled
    if ($need_session || not defined $need_session) {
        if (my $method = $request->can('get_session')) {
            $session = $request->$method();
        }
    }

    if ($need_session && not defined $session) {
        JasPerl::Util::IllegalStateException->throw("no session available");
    }

    # set up page scope
    my $attributes = JasPerl::Util::Attributes->new(
        APPLICATION() => $context,
        CONFIG()      => $config,
        OUT()         => $out,
        PAGE()        => $page,
        PAGECONTEXT() => $self,
        REQUEST()     => $request,
        RESPONSE()    => $response
    );

    if ($session) {
        $attributes->set_attribute(SESSION, $session);
    }

    $self->{out} = $out;
    $self->{bodies} = [ $out ];
    $self->{scopes} = [ $attributes, $request, $session, $context ];

    # improve implicit objects access
    @{$self}{qw(config context errpage page request response session)} = (
        $config, $context, $errpage, $page, $request, $response, $session
    );
}

sub release {
    my $self = shift;
    $self->{out}->flush() if $self->{out};
    %{$self} = ( );
}

sub find_attribute {
    my ($self, $name) = @_;
    JasPerl::Util::NullPointerException->throw()
        unless defined $name;

    foreach my $object (@{$self->{scopes}}) {
        next unless defined $object;
        my $value = $object->get_attribute($name);
        return $value if defined $value;
    }

    return;
}

sub get_attribute {
    my ($self, $name, $scope) = @_;
    JasPerl::Util::NullPointerException->throw()
        unless defined $name;
    $scope ||= 0; # default is page scope

    if (my $object = $self->{scopes}->[$scope]) {
        return $object->get_attribute($name);
    } elsif (my $message = $INVALID_SCOPE_MESSAGES[$scope]) {
        JasPerl::Util::IllegalStateException->throw($message);
    } else {
        JasPerl::Util::IllegalArgumentException->throw();
    }
}

sub set_attribute {
    my ($self, $name, $value, $scope) = @_;
    JasPerl::Util::NullPointerException->throw()
        unless defined $name;
    $scope ||= 0; # default is page scope

    if (my $object = $self->{scopes}->[$scope]) {
        $object->set_attribute($name, $value);
    } elsif (my $message = $INVALID_SCOPE_MESSAGES[$scope]) {
        JasPerl::Util::IllegalStateException->throw($message);
    } else {
        JasPerl::Util::IllegalArgumentException->throw();
    }
}

sub remove_attribute {
    my ($self, $name, $scope) = @_;
    JasPerl::Util::NullPointerException->throw()
        unless defined $name;

    if (not defined $scope) {
        foreach my $object (grep { defined } @{$self->{scopes}}) {
            $object->remove_attribute($name);
        }
    } elsif (my $object = $self->{scopes}->[$scope]) {
        $object->remove_attribute($name);
    } elsif (my $message = $INVALID_SCOPE_MESSAGES[$scope]) {
        JasPerl::Util::IllegalStateException->throw($message);
    } else {
        JasPerl::Util::IllegalArgumentException->throw();
    }
}

sub get_attribute_names_in_scope {
    my ($self, $scope) = @_;
    JasPerl::Util::NullPointerException->throw()
        unless defined $scope;

    if (my $object = $self->{scopes}->[$scope]) {
        $object->get_attribute_names();
    } elsif (my $message = $INVALID_SCOPE_MESSAGES[$scope]) {
        JasPerl::Util::IllegalStateException->throw($message);
    } else {
        JasPerl::Util::IllegalArgumentException->throw();
    }
}

sub get_attributes_scope {
    my ($self, $name) = @_;
    JasPerl::Util::NullPointerException->throw()
        unless defined $name;

    foreach my $scope (0 .. $#{$self->{scopes}}) {
        my $object = $self->{scopes}->[$scope];
        next unless $object;

        my $value = $object->get_attribute($name);
        return $scope if defined $value;
    }

    return -1;
}

sub get_out {
    return $_[0]->{bodies}->[-1] || $_[0]->{out};
}

sub push_body {
    my ($self, $out) = @_;

    my $body = JasPerl::JspWriter->from($out);
    $self->set_attribute(OUT, $body);
    push @{$self->{bodies}}, $body;
    return $body;
}

sub pop_body {
    my $self = shift;
    my $body = pop @{$self->{bodies}} || $self->{out};
    $self->set_attribute(OUT, $body);
    return $body;
}

sub get_error_data {
    my $request = $_[0]->get_request();

    return JasPerl::ErrorData->new(
        $request->get_attribute(JasPerl::RequestDispatcher::ERROR_EXCEPTION),
        $request->get_attribute(JasPerl::RequestDispatcher::ERROR_STATUS_CODE),
        $request->get_attribute(JasPerl::RequestDispatcher::ERROR_REQUEST_URI),
        $request->get_attribute(JasPerl::RequestDispatcher::ERROR_SERVLET_NAME)
    );
}

sub get_exception {
    return $_[0]->{request}->get_attribute(EXCEPTION);
}

sub get_page {
    return $_[0]->{page};
}

sub get_request {
    return $_[0]->{request};
}

sub get_response {
    return $_[0]->{response};
}

sub get_session {
    return $_[0]->{session};
}

sub forward {
    my ($self, $path) = @_;

    eval {
        $self->{out}->clear();
    };
    if ($@) {
        JasPerl::Util::IllegalStateException->throw("cannot clear buffer", $@);
    }

    my $rd = $self->{request}->get_request_dispatcher($path)
        or JasPerl::JspException->throw("no request dispatcher for '$path'");
    $rd->forward($self->{request}, $self->{response});
}

sub include {
    my ($self, $path, $flush) = @_;
    $flush = 1 unless defined $flush;

    my $out = $self->get_out();
    $out->flush() if $flush; # TODO: BodyContent?

    my $rd = $self->{request}->get_request_dispatcher($path)
        or JasPerl::JspException->throw("no request dispatcher for '$path'");
    # TODO: need to wrap response for get_writer() -> current out
    $rd->include($self->{request}, $self->{response});
}

sub handle_page_exception {
    my ($self, $e) = @_;
    JasPerl::Util::NullPointerException->throw()
        unless defined $e;
    JasPerl::JspException->from($e)->throw()
        unless $self->{errpage};

    my $sc = JasPerl::Response::SC_INTERNAL_SERVER_ERROR;
    my $uri = $self->{request}->get_request_uri();
    my $name = $self->{config}->get_name();

    my $request = $self->{request};
    $request->set_attribute(EXCEPTION, $e);
    $request->set_attribute(JasPerl::RequestDispatcher::ERROR_EXCEPTION, $e);
    $request->set_attribute(JasPerl::RequestDispatcher::ERROR_STATUS_CODE, $sc);
    $request->set_attribute(JasPerl::RequestDispatcher::ERROR_REQUEST_URI, $uri);
    $request->set_attribute(JasPerl::RequestDispatcher::ERROR_SERVLET_NAME, $name);

    eval {
        $self->forward($self->{errpage});
    };
    if (my $e = JasPerl::Util::IllegalStateException->caught()) {
        $self->include($self->{errpage});
    } elsif ($@) {
        JasPerl::JspException->from($@)->throw();
    }

    $request->remove_attribute(EXCEPTION);
    $request->remove_attribute(JasPerl::RequestDispatcher::ERROR_EXCEPTION);
    $request->remove_attribute(JasPerl::RequestDispatcher::ERROR_STATUS_CODE);
    $request->remove_attribute(JasPerl::RequestDispatcher::ERROR_REQUEST_URI);
    $request->remove_attribute(JasPerl::RequestDispatcher::ERROR_SERVLET_NAME);
}

sub get_variable_resolver {
    my $self = shift;

    return sub {
        my $var = shift;
        if (exists $self->{implicit}->{$var}) {
            return $self->{implicit}->{$var};
        } elsif (my $method = $IMPLICIT_OBJECTS{$var}) {
            return $self->{implicit}->{$var} = $self->$method();
        } else {
            return $self->find_attribute($var);
        }
    };
}

sub get_scope_by_name {
    my ($class, $name) = @_;
    JasPerl::Util::IllegalArgumentException->throw("invalid scope: '$name'")
        unless exists $NAMED_SCOPES{$name};
    return $NAMED_SCOPES{$name};
}

sub DESTROY {
    $_[0]->release();
}

1;
